Architektura API pro generování XML

Přehled tří přístupů

Tento dokument shrnuje diskuzi o různých architektonických přístupech k vytváření XML dokumentů v XmlStackBuilder, zejména s ohledem na použití z FoxPro přes wwDotnetBridge.

1. Stack API (současná implementace)

Princip:

Příklad použití (FoxPro):

LOCAL loBridge, loBuilder

loBridge = getwwBridge()
loBridge.LoadAssembly("XmlStackBuilder.Core.dll")
loBuilder = loBridge.CreateInstance("XmlStackBuilder.Core.XmlStackBuilder")

loBuilder.Push("Hlavicka")
loBuilder.PushValue("mesic", "12")
loBuilder.PushValue("rok", "2024")
loBuilder.Pop()  && ukončí Hlavicka

lcXml = loBuilder.Build()

Výhody pro FoxPro:

Nevýhody:

2. Object Tree API

Princip:

Příklad použití (C#):

var root = new XmlElement("Hlavicka");
var mesic = new XmlElement("mesic");
mesic.SetValue("12");
root.AddChild(mesic);

var rok = new XmlElement("rok");
rok.SetValue("2024");
root.AddChild(rok);

string xml = root.ToXml();

Příklad použití (FoxPro):

LOCAL loBridge, loRoot, loMesic, loRok

loBridge = getwwBridge()
loBridge.LoadAssembly("XmlStackBuilder.Core.dll")

loRoot = loBridge.CreateInstance("XmlStackBuilder.Core.XmlElement", "Hlavicka")

* Vytvoření child elementu
loMesic = loBridge.CreateInstance("XmlStackBuilder.Core.XmlElement", "mesic")
loMesic.SetValue("12")
loRoot.AddChild(loMesic)

* Další child element
loRok = loBridge.CreateInstance("XmlStackBuilder.Core.XmlElement", "rok")
loRok.SetValue("2024")
loRoot.AddChild(loRok)

lcXml = loRoot.ToXml()

Výhody:

Nevýhody pro FoxPro:

3. Fluent Builder API

Princip:

Příklad použití (C#):

var xml = new FluentXmlBuilder()
    .WithRoot("Dphdp3")
    .WithHlavicka(h => {
        h.Mesic(12);
        h.Rok(2024);
        h.DatVytvor(DateTime.Now);
    })
    .WithPolozky(p => {
        p.AddPolozka(pol => {
            pol.Castka(1500.50m);
            pol.Popis("Test");
        });
    })
    .Build();

Důvody, proč NELZE použít z FoxPro:

  1. FoxPro nemá lambda výrazy

    // C# lambda - neexistuje ekvivalent ve FoxPro
    .WithHlavicka(h => { h.Mesic(12); })
    
  2. FoxPro nepodporuje Action delegáty přes COM

    // Tento typ parametru není přenositelný přes COM
    public FluentXmlBuilder WithHlavicka(Action<HlavickaBuilder> configure)
    
  3. FoxPro nemá generické typy

    // Generika nefungují přes COM interop
    public class FluentXmlBuilder<T> where T : class
    
  4. Method chaining nefunguje přes COM

    * Toto NEFUNGUJE ve FoxPro:
    loBuilder.WithRoot("Test").WithHlavicka(...).Build()
    

Výhody (pouze pro C#):

Nevýhody:

Srovnání pro FoxPro + COM

Vlastnost Stack API Object Tree Fluent Builder
Použitelnost z FoxPro ✅ Ano ⚠️ Možné, ale pomalé ❌ Ne
Počet COM objektů 1 (builder) Mnoho (každý element) N/A
Method chaining Nepotřebuje Nefunguje v FoxPro Nefunguje v FoxPro
Lambda výrazy Nepotřebuje Nepotřebuje Vyžaduje (FoxPro nemá)
Výkon přes COM ⚡ Rychlé 🐌 Pomalé N/A
Čitelnost kódu Dobrá Verbose N/A

Doporučení

Pro FoxPro klienty (wwDotnetBridge)

Používejte Stack API - je to jediný praktický přístup pro FoxPro:

Pro C# klienty

Můžete použít všechny tři přístupy:

Závěr

Stack API je optimální volba pro XmlStackBuilder, protože:

  1. Funguje perfektně z FoxPro (primární use case)
  2. Je dostatečně jednoduchý pro C# klienty
  3. Minimalizuje komunikační overhead přes COM
  4. Nevyžaduje pokročilé .NET funkce (lambdas, generics)

FoxPro omezení, která vylučují alternativní přístupy:

Stack API je jediná architektura, která splňuje požadavky pro použití z FoxPro přes wwDotnetBridge.


API pro generování šablon a výstupů

Kromě Stack API pro sestavování XML nabízí XmlStackBuilder další API pro generování JSON šablon, renderování výstupů a konverzi FRX reportů.

Generování JSON šablon z cursor schema (CursorSchemaGenerator)

Automatické generování JSON šablon pro tabulkové sestavy z definice FoxPro kurzoru — bez nutnosti psát JSON ručně.

Dvě metody

Metoda Vstup Výstup
GenerateColumnsFromSchema cursor schema + options JSON fragment s columns + printColumns
GenerateReportFromSchema output path + cursor schema + options kompletní JSON soubor s report, groups, sumExpressions

Cursor schema — dva formáty

*--- CREATE TABLE formát ---
lcSchema = "CREATE TABLE U (Den D, Rada C(2), Doklad N(5), Castka N(15,2), Text C(30))"

*--- Plain formát ---
lcSchema = "Den D,Rada C(2),Doklad N(5),Castka N(15,2),Text C(30)"

*--- S FoxPro line continuation ---
lcSchema = "CREATE TABLE U (Den D;" + CHR(13) + CHR(10) + "  , Rada C(2))"

Podporované typy: C/Character, N/Numeric, D/Date, T/DateTime, L/Logical, M/Memo, I/Integer, B/Double, Y/Currency, V/Varchar, F/Float.

Options string

Formát: "key1:value1,key2:value2,..." — split na , (respektuje {}), pak na první :.

Klíč Default Popis
dataFontPt 8 Velikost fontu datových buněk (pt)
pageSize A4 Formát papíru: A4, A3
orientation auto portrait, landscape, auto (zkusí portrait, pak landscape)
maxDecimal 2 Max počet desetinných míst
maxLenString 20 Max šířka textových sloupců (znaky)
groups (prázdný) Skupiny: stredisko;ucet+organizace;sn
noTotalColumns (prázdný) Sloupce bez součtů: id;doklad;radek
element data Název XML elementu se záznamy
title (prázdný) Nadpis sestavy (podporuje {element.field})
tfiltr (prázdný) Filtr sestavy (podporuje {element.field})
culture cs-CZ Kultura pro formátování
rowNumbers false Číslování řádků: false, global, group

Groups — automatické generování skupin

Syntax: groups:stredisko;ucet+organizace;sn

Odhad šířek sloupců

Šířky se počítají v mm podle vzorce inspirovaného sfReport/makerepo2:

charWidthMm = fontPt × 0.6 × 25.4 / 72.0
gapMm       = 2 × (0.5 - 0.015 × fontPt) × charWidthMm
columnMm    = widthChars × charWidthMm + gapMm

Šířky v znacích podle typu:

Typ Šířka (znaky) Poznámka
D (Date) 8.5 dd.MM.yyyy
T (DateTime) 14 dd.MM.yyyy HH:mm
L (Logical) 3 Ano/Ne
C (Char) min(max(width, 2), maxLenString) Krátké (≤4) × 1.2
N (Numeric) picture 999 999.99 Korekce: mezery×0.35, tečky×0.45
I (Integer) min(9, width) S tisícovým formátem
Y (Currency) 10+2 999 999 999.99

Finální šířka = max(dataWidthMm, headerWidthMm).

printColumns

Dostupná šířka stránky (po odečtení 2×8mm margins):

Papír Portrait Landscape
A4 194 mm 281 mm
A3 281 mm 404 mm

Sloupce se přidávají zleva dokud kumulativní šířka nepřesáhne dostupnou šířku. Pokud se nevejdou všechny → "printColumns" v JSON.

Auto orientation ("auto"): zkusí portrait, pokud se nevejdou → landscape.

FoxPro příklady

*--- 1. Fragment columns (pro vložení do vlastní šablony) ---
lcColumns = loBuilder.GenerateColumnsFromSchema( ;
    "Den D,Rada C(2),Castka N(15,2)", ;
    "dataFontPt:8")

* Vrátí JSON:
* {
*   "columns": {
*     "den": { "label": "Den", "align": "center", "format": "dd.MM.yyyy", "width": "13.8mm" },
*     "rada": { "label": "Rada", "width": "7.1mm" },
*     "castka": { "label": "Castka", "align": "right", "format": "N2", "hideZero": true, "width": "25.6mm" }
*   }
* }


*--- 2. Kompletní report s groups a placeholdery ---
loBuilder.GenerateReportFromSchema( ;
    "C:\output\report.json", ;
    "CREATE TABLE U (Den D, Rada C(2), Stredisko C(3), Ucet C(3), " + ;
    "Castka N(15,2), Cizi N(15,2), Text C(30))", ;
    "dataFontPt:7" + ;
    ",groups:stredisko;ucet" + ;
    ",noTotalColumns:den;rada;stredisko;ucet;text" + ;
    ",title:{header.nadpis}" + ;
    ",tfiltr:{header.filtr}" + ;
    ",element:doklady")

* Vygeneruje kompletní JSON šablonu:
*   - renderer: "table"
*   - report: title, culture, dataFontPt, pageLayout, pageMarginMm
*   - header: filter s placeholderem
*   - pageHeader/pageFooter: {company}, {title}, {page}/{pages}, {date}
*   - sections: element, columns s šířkami, 2 groups se sumExpressions, grand total
*   - printColumns (pokud se nevejdou všechny)


*--- 3. Saldokonto s auto orientací ---
loBuilder.GenerateReportFromSchema( ;
    "C:\output\saldo.json", ;
    "Den D,Organizace C(15),Doklad C(10),MD N(15,2),D N(15,2),Saldo N(15,2)", ;
    "dataFontPt:6,orientation:auto,groups:organizace,noTotalColumns:den;doklad,element:pohyby")


*--- 4. Jednoduchý ceník bez skupin ---
loBuilder.GenerateReportFromSchema( ;
    "C:\output\cenik.json", ;
    "Kod C(10),Nazev C(40),Mj C(5),Cena N(12,2)", ;
    "noTotalColumns:kod;nazev;mj,element:polozky,title:Ceník zboží")


*--- 5. Široká sestava s compound skupinou ---
loBuilder.GenerateReportFromSchema( ;
    "C:\output\analyza.json", ;
    "Stredisko C(3),Ucet C(6),Organizace C(10),Doklad C(10),Text C(30)," + ;
    "MD N(15,2),D N(15,2),Zustatek N(15,2)", ;
    "dataFontPt:6" + ;
    ",orientation:landscape" + ;
    ",pageSize:A4" + ;
    ",groups:stredisko;ucet+organizace" + ;
    ",noTotalColumns:doklad;text" + ;
    ",element:pohyby" + ;
    ",culture:cs-CZ" + ;
    ",rowNumbers:group")

* Výsledek: compound skupina ucet+organizace →
*   "groupExpression": ["ucet", "organizace"]
*   "label": "Ucet + Organizace {ucet} {organizace}"

Příklad generovaného JSON

{
  "renderer": "table",
  "report": {
    "title": "{header.nadpis}",
    "culture": "cs-CZ",
    "dataFontPt": 7,
    "pageLayout": "paged",
    "pageMarginMm": 8
  },
  "header": {
    "filter": "{header.filtr}"
  },
  "pageHeader": {
    "left": "{company}",
    "center": "{title}",
    "right": "Strana {page} z {pages}",
    "heightMm": 10,
    "fromPage": 2
  },
  "pageFooter": {
    "left": "Vytištěno: {date}",
    "heightMm": 8
  },
  "sections": [
    {
      "element": "doklady",
      "columns": {
        "den": { "label": "Den", "align": "center", "format": "dd.MM.yyyy", "width": "13.8mm" },
        "rada": { "label": "Rada", "width": "7.1mm" },
        "stredisko": { "label": "Stredisko", "width": "14.5mm" },
        "ucet": { "label": "Ucet", "width": "7.1mm" },
        "castka": { "label": "Castka", "align": "right", "format": "N2", "hideZero": true, "width": "25.6mm" },
        "cizi": { "label": "Cizi", "align": "right", "format": "N2", "hideZero": true, "width": "25.6mm" },
        "text": { "label": "Text", "width": "30.8mm" }
      },
      "groups": [
        {
          "groupExpression": "stredisko",
          "label": "Stredisko {stredisko}",
          "headerMode": "external",
          "tableBreak": true,
          "sumExpressions": [
            { "field": "castka", "expr": "SUM(castka)", "format": "N2" },
            { "field": "cizi", "expr": "SUM(cizi)", "format": "N2" }
          ],
          "sumLabel": "Celkem za {stredisko}"
        },
        {
          "groupExpression": "ucet",
          "label": "Ucet {ucet}",
          "headerMode": "inline",
          "tableBreak": false,
          "sumExpressions": [
            { "field": "castka", "expr": "SUM(castka)", "format": "N2" },
            { "field": "cizi", "expr": "SUM(cizi)", "format": "N2" }
          ],
          "sumLabel": "Celkem za {ucet}"
        }
      ],
      "sumExpressions": [
        { "field": "castka", "expr": "SUM(castka)", "format": "N2" },
        { "field": "cizi", "expr": "SUM(cizi)", "format": "N2" }
      ],
      "sumLabel": "Celkem"
    }
  ],
  "_info": "Vygenerováno z cursor schema. Upravte element, labels a formáty dle potřeby."
}

Doporučené šířky sloupců v mm

Šířky se počítají podle vzorce:

charWidthMm = fontPt × 0.6 × 25.4 / 72
gapMm       = 2 × (0.5 − 0.015 × fontPt) × charWidthMm
paddingMm   = 2.1                           (CSS: 2×4px padding, fixed)
columnMm    = effWidth × charWidthMm + gapMm + paddingMm

Padding 2.1 mm = CSS td{padding:2px 4px} s border-collapse:collapse. Je uvnitř šířky sloupce, nezávisí na fontu. Korekce pro numerické sloupce: mezera (tisíce) = 0.35 znaku, tečka/čárka (des.) = 0.45 znaku.

Konstanty:

Font charWidthMm gapMm gap+padding
6 pt 1.270 1.04 3.14
7 pt 1.482 1.17 3.27
8 pt 1.693 1.29 3.39
9 pt 1.905 1.39 3.49

Šířky sloupců:

Typ Obraz (picture) effWidth 6 pt 7 pt 8 pt 9 pt
Date dd.MM.yyyy 8.50 13.9 15.9 17.8 19.7
DateTime dd.MM.yyyy HH:mm 14.00 20.9 24.0 27.1 30.2
L A/N 3.00 7.0 7.7 8.5 9.2
N0 (3) 999 3.00 7.0 7.7 8.5 9.2
N0 (6) 999 999 6.65 11.6 13.1 14.6 16.2
N0 (9) 999 999 999 10.30 16.2 18.5 20.8 23.1
N0 (12) 999 999 999 999 13.95 20.9 23.9 27.0 30.1
N2 (3) 999,99 5.55 10.2 11.5 12.8 14.1
N2 (6) 999 999,99 9.20 14.8 16.9 19.0 21.0
N2 (9) 999 999 999,99 12.85 19.5 22.3 25.1 28.0
N2 (12) 999 999 999 999,99 16.50 24.1 27.7 31.3 34.9
N3 (3) 999,999 6.55 11.5 13.0 14.5 16.0
N3 (6) 999 999,999 10.20 16.1 18.4 20.7 22.9
N3 (9) 999 999 999,999 13.85 20.7 23.8 26.8 29.9
N3 (12) 999 999 999 999,999 17.50 25.4 29.2 33.0 36.8
C(4) xxxx (×1.2) 4.80 9.2 10.4 11.5 12.6
C(6) xxxxxx 6.00 10.8 12.2 13.5 14.9
C(10) xxxxxxxxxx 10.00 15.8 18.1 20.3 22.5
C(20) 20 znaků 20.00 28.5 32.9 37.3 41.6
C(30) 30 znaků 30.00 41.2 47.7 54.2 60.6
C(40) 40 znaků 40.00 53.9 62.5 71.1 79.7
C(50) 50 znaků 50.00 66.6 77.4 88.1 98.7

Dostupná šířka stránky (po odečtení 2×8 mm okrajů):

Formát Portrait Landscape
A4 194 mm 281 mm
A3 281 mm 404 mm

Poznámka: CursorSchemaGenerator standardně ořezává textové sloupce na maxLenString=20 znaků. C(30)–C(50) se tedy v generovaném JSON zobrazí jako 20 znaků, pokud nenastavíte maxLenString:30 (nebo vyšší) v options.

Přepočet šířek při změně velikosti písma

Při změně dataFontPt je potřeba přepočítat "width" na sloupcích.

Přesný vzorec (padding = fixní, content + gap = škálují se):

paddingMm = 2.1
width_nové = paddingMm + (width_staré - paddingMm) × (fontPt_nové / fontPt_staré)

Zjednodušený vzorec (chyba < 0.5 mm pro sloupce > 10 mm):

width_nové ≈ width_staré × (fontPt_nové / fontPt_staré)

Přepočítávací koeficienty:

Z \ Na 6 pt 7 pt 8 pt 9 pt
6 pt 1.167 1.333 1.500
7 pt 0.857 1.143 1.286
8 pt 0.750 0.875 1.125
9 pt 0.667 0.778 0.889

Koeficienty = prostý poměr fontů (fontPt_nové / fontPt_staré): 7/8 = 0.875, 6/8 = 0.75 atd. Přesný vzorec s paddingem se vyplatí u úzkých sloupců (< 12 mm).

Typický workflow

1. FoxPro: loBuilder.GenerateReportFromSchema("sablona.json", lcSchema, lcOptions)
2. Uživatel: otevře sablona.json ve VS Code, upraví labels, přidá lookup, vizuální úpravy
3. FoxPro: loBuilder.Render("sestava.html", "sablona.json")   && nebo .pdf / .xlsx

Sjednocené Render API

*--- XML z interního builderu ---
loBuilder.Render("sestava.html", "definice.json")   && HTML výstup
loBuilder.Render("sestava.pdf", "definice.json")     && PDF výstup
loBuilder.Render("sestava.xlsx", "definice.json")    && XLSX výstup
loBuilder.Render("PRINTER:", "definice.json")         && tisk na výchozí tiskárnu

*--- XML z externího zdroje ---
loBuilder.RenderFromFiles("sestava.html", "definice.json", "data.xml")

Triple auto-detekce:

FRX konverze

*--- FRX XML export → JSON šablona (auto-detekce typu) ---
lcJson = loBuilder.ConvertFrxToJson(lcFrxXml)

*--- Všechny 3 soubory (JSON + vzorový XML + PRG) ---
loBuilder.ConvertFrxToAllFiles("C:\frx\CENIK.xml", "C:\output\")

*--- Vynucený typ (obchází auto-detekci) ---
loBuilder.ConvertFrxToDocumentJsonFile("C:\frx\INVUCKC.xml", "C:\output\invuckc.json")
loBuilder.ConvertFrxToTableReportJsonFile("C:\frx\CENIK.xml", "C:\output\cenik.json")

*--- Vynucený typ — všechny 3 soubory ---
loBuilder.ConvertFrxToDocumentAllFiles("C:\frx\INVUCKC.xml", "C:\output\")
loBuilder.ConvertFrxToTableReportAllFiles("C:\frx\CENIK.xml", "C:\output\")

*--- Vzorový XML a FoxPro PRG z JSON šablony ---
lcXml = loBuilder.GenerateSampleXmlFromJson(lcJson)
lcPrg = loBuilder.GenerateFoxProCodeFromJson(lcJson)

Auto-detekce typu (table vs. document) se řídí výškami bandů a počtem polí v FRX. Pro případy kdy detekce selže, existují varianty s vynuceným typem (TableReport/Document v názvu metody).

Placeholdery {element.field} v report properties

Všechny string properties v JSON sekcích report, header, footer, pageHeader, pageFooter podporují {element.field} placeholdery. Resolvují se při renderování z XML dat.

{
  "report": {
    "orientation": "{parametry.orientace}",
    "dataFontPt": "{parametry.fontpt}",
    "culture": "{parametry.kultura}"
  }
}
loBuilder.Push("parametry")
loBuilder.AddAttribute("orientace", "landscape")
loBuilder.AddAttribute("fontpt", "6")
loBuilder.AddAttribute("kultura", "en-US")
loBuilder.Pop()

loBuilder.Render("sestava.html", "definice.json")
&& → orientation=landscape, dataFontPt=6, culture=en-US

VS Code Editor s live preview

loBuilder.OpenReportEditor("C:\json\sablona.json", "C:\data\sample.xml")

Otevře VS Code s:

ShowProgress — průběh renderování

loBuilder.ShowProgress = .T.
loBuilder.Render("sestava.pdf", "sablona.json")   && dialog s průběhem
loBuilder.ShowProgress = .F.                        && vypnout

WinForms dialog na samostatném STA threadu: